Agregar TLS a la instalación de Jenkins-x

2020-03-08

Hola aragon. Bueno para empezar tenemos que definir por que esto podría no ser posible o difícil de logar de manera automática.

El soporte para TLS en Jenkins-x es solo para GKS y EKS ya que si nosotros lo intentamos instalar tenemos que ver que vamos a cambiar y entender que componente usa jenkins-x.

Pre-requito

Tener un cluster de kubernetes funcionando en algún cloud provider y jenkins-x instalado, yo utilizare Digital Ocean ya que es el que tengo disponible.

Modificat archivo jx-requirements.yaml

Para iniciar tenemos que cambiar la configuración para habilitar TLS en la llave de ingress tenemos que modificar.

  • domain: nuestro dominio o sub dominio.
  • namespaceSubDomain: para indicar que subDomain queremos utilizar en dev.
  • tls.email: requisito de Let’s Encrypt
  • tls.enable: true ya que lo queremos habilitar.
  • tls.production: de momento lo dejamos en false ya que como estamos experimentando debemos usar el servidor de staging ya que el de producción tiene rate limits.
ingress:
  domain: apps.beethub.com.mx
  externalDNS: false
  namespaceSubDomain: dev.
  tls:
    email: contacto.beet@gmail.com
    enabled: true
    production: false

dentro de la el key de environments.ingress tenemos que hacer las mismas modificaciones en el ambiente de dev.

environments:
- ingress:
    domain: apps.beethub.com.mx
    externalDNS: false
    namespaceSubDomain: dev.
    tls:
      email: contacto.beet@gmail.com
      enabled: true
      production: false

bueno llego el momento jx boot --verbose para que podamos ver en detalle lo que esta haciendo la instalación. Como era de esperarse hay un error.

Análisis del error.

The Issuer "letsencrypt-staging" is invalid:
spec.acme.solvers.dns01 in body must be of type object: "null"'

Revisando el log me di cuenta de 2 cosas la primera es que copia desde system/acme hacia una carpeta temporal y después sustituye los valores, si vemos el archivo:

cert-manager-staging-issuer.yaml

apiVersion: cert-manager.io/v1alpha2
kind: Issuer
metadata:
  name: letsencrypt-staging
spec:
  acme:
    server: https://acme-staging-v02.api.letsencrypt.org/directory
    email: "{{ .Values.certmanager.email }}"
    # Name of a secret used to store the ACME account private key
    privateKeySecretRef:
      name: letsencrypt-staging
    solvers:
    - selector:
        dnsNames:
        - "*.{{ .Values.cluster.domain }}"
        - "{{ .Values.cluster.domain }}"
      # ACME DNS-01 provider configurations
      dns01:
{{- if eq .Values.cluster.provider "gke" }}
        clouddns:
          # The project in which to update the DNS zone
          project: "{{ .Values.cluster.projectID }}"
          # A secretKeyRef to a google cloud json service account
          serviceAccountSecretRef:
            name: external-dns-gcp-sa
            key: credentials.json
{{- end }}
{{- if eq .Values.cluster.provider "eks" }}
        route53:
          region: {{ .Values.cluster.region }}
{{- end }}
{{- end }}
{{- end }}

primero tenemos que ver 2 cosas la primera es que están utilizando un objecto “Issuer” esto quiere decir que están usando un Custom Resources o crd y también puedes ver que están utilizando un componente externo cert-manager para lo siguiente tuve que leer un poco e investigar como es que funciona cert-manager mis fuentes fueron primero una lista de reproducción de Youtube que te explica como es que TLS funciona hasta hacer una implementación de cert-manager kubucation youtube channel y lo siguiente es ver como que funciona el dns01 challenge podríamos probar con el http pero cert-manager Digital Ocean tiene configuración para usar dns01 veamos si hacemos que funcione.

Configuración para Digital Ocean

Según la documentación necesitamos un secret en ejemplo nos colocan uno que se llama digitalocean-dns me gusta el nombre con un key de access-token.

This provider uses a Kubernetes Secret resource to work. In the following example, the Secret will have to be named digitalocean-dns and have a sub-key access-token with the token in it.

apiVersion: cert-manager.io/v1alpha2
kind: Issuer
metadata:
  name: example-issuer
spec:
  acme:
    ...
    solvers:
    - dns01:
        digitalocean:
          tokenSecretRef:
            name: digitalocean-dns
            key: access-token

El secret lo tenemos que crear en el namespace jx

error: upgrading helm chart '.': failed to run 'kubectl apply --recursive -f /tmp/helm-template-workdir-426612810/acme/output/namespaces/jx -l jenkins.io/chart-release=acme --namespace jx --wait --validate=false'

Ok entonces creemos el Secret al no tener vault para almacenarlos pues tenemos que crearlo de manera manual.

apiVersion: v1
kind: Secret
metadata:
  name:  digitalocean-dns
stringData:
   access-token: ${access-token}
type: Opaque

ahora hay que modificar el archivo cert-manager-staging-issuer para colocar la configuración de Digital Ocean, quedaría de la siguiente manera. como pueden ver elimine las condiciones de gke y eks ya que no lo utilizo.

{{- if .Values.certmanager.enabled }}
{{- if eq .Values.certmanager.production "false" }}
apiVersion: cert-manager.io/v1alpha2
kind: Issuer
metadata:
  name: letsencrypt-staging
spec:
  acme:
    server: https://acme-staging-v02.api.letsencrypt.org/directory
    email: "{{ .Values.certmanager.email }}"
    # Name of a secret used to store the ACME account private key
    privateKeySecretRef:
      name: letsencrypt-staging
    solvers:
    - selector:
        dnsNames:
        - "*.{{ .Values.cluster.domain }}"
        - "{{ .Values.cluster.domain }}"
      # ACME DNS-01 provider configurations
      dns01:
        digitalocean:
          tokenSecretRef:
            name: digitalocean-dns
            key: access-token
{{- end }}
{{- end }}

veamos que pasa es tiempo de volver a ejecutar jx boot --verbose pues para mi la instalación se completo sin problema y las urls se cambiaron a HTTPS aun que no funcionan el Issuer ya que si verifico si el certificado esta listo para ser usado dice que no kubectl describe certificate tls-apps-beethub-com-mx-s

Status:
  Conditions:
    Last Transition Time:  2020-03-08T20:19:04Z
    Message:               Waiting for CertificateRequest "tls-apps-beethub-com-mx-s-234585427" to complete
    Reason:                InProgress
    Status:                False
    Type:                  Ready
Events:                    <none>

lo siguiente fue ver los eventos kubectl get events en el cual encontré otro log:

2m52s Warning PresentError challenge/tls-apps-beethub-com-mx-s-234585427-3522979291-3250367871 Error presenting challenge: POST https://api.digitalocean.com/v2/domains/beethub.com.mx/records: 404 The resource you were accessing could not be found.

revisando el API de digital ocean veo que es para crear un registro en el dominio de beethub.com.mx así que ahora toca revisar el pod del controller kubectl logs cm-cert-manager-5f5c9cc976-6wrn4 -n cert-manager aquí encontré el siguiente error.

E0308 23:00:23.165037 1 controller.go:131] cert-manager/controller/challenges "msg"="re-queuing item due to error processing" "error"="POST https://api.digitalocean.com/v2/domains/beethub.com.mx/records: 404 The resource you were accessing could not be found." "key"="jx/tls-apps-beethub-com-mx-s-234585427-3522979291-3250367871"

entonces creo que tengo que crear el domain en Digital ocean es decir no se crea solo entonces cree el domain en DigitalOcean pero eso no quiere decir que el challenge se vaya a resolver veamos si se agrega el record sin necesidad de iniciar un nuevo pipeline o si marca algun error.

al agregar el dominio a digital ocean se agrego el TXT record despues de unos minutos lo siguiente es esperar que el TXT record sea visible en la web.

E0308 23:24:13.814983 1 sync.go:184] cert-manager/controller/challenges "msg"="propagation check failed" "error"="DNS record for \"apps.beethub.com.mx\" not yet propagated" "dnsName"="apps.beethub.com.mx" "resource_kind"="Challenge" "resource_name"="tls-apps-beethub-com-mx-s-234585427-3522979291-3250367871" "resource_namespace"="jx" "type"="dns-01"

Así que para esto apunte los dns del subdominio de apps a digital ocean y en el dominio agregue los registros A con la dirrecion Ip de el balanceador de carga.

Al hacer el lookup ya veo el registro TXT, y el dns resuelve desde mi computadora aun que en el log solo veo

E0309 00:34:09.612552 1 sync.go:184] cert-manager/controller/challenges "msg"="propagation check failed" "error"="DNS record for \"apps.beethub.com.mx\" not yet propagated" "dnsName"="apps.beethub.com.mx" "resource_kind"="Challenge" "resource_name"="tls-apps-beethub-com-mx-s-234585427-3522979291-3250367871" "resource_namespace"="jx" "type"="dns-01"

Seguí buscando información pero al rededor de 1 hora después de que agregue el DNS y era visible el record en el DNS se quito el error y ya se puede ver de manera correcta el Issuer kubectl describe Issuer

Status:
  Acme:
    Last Registered Email:  contacto.beet@gmail.com
    Uri:                    https://acme-staging-v02.api.letsencrypt.org/acme/acct/12717811
  Conditions:
    Last Transition Time:  2020-03-08T22:39:13Z
    Message:               The ACME account was registered with the ACME server
    Reason:                ACMEAccountRegistered
    Status:                True
    Type:                  Ready
Events:                    <none>

Momento de pasar a al servidor de producción de Let’s Encrypt para esto solo vamos a cambiar la propiedad ingress.production a true y con eso deberia ser suficiente para ya obtener un certificado valido, no olvidemos que tenemos que modificar el archivo cert-manager-prod-issuer.yaml ya que tenemos que colocar el mismo secret.

Como comentario adicional tendría que comentar que cambie el Kind a ClusterIssuer ya que los Issuer es por Namespace es decir para cada namespace que tengamos tendríamos que crear un Issuer me pareció mejor idea crear un ClusterIssuer y solo generar un certificado por namespace.


Escrito por Cesar Adrian Varela Guerrero quien vive y trabaja para construir cosas utiles. siguelo en twitter